// Copyright (c) 2015-2016 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#include <httprpc.h>

#include <chainparams.h>
#include <common/args.h>
#include <config.h>
#include <crypto/hmac_sha256.h>
#include <logging.h>
#include <rpc/protocol.h>
#include <util/strencodings.h>
#include <util/string.h>
#include <util/translation.h>
#include <walletinitinterface.h>

#include <boost/algorithm/string.hpp>

#include <algorithm>
#include <cstdio>
#include <functional>
#include <iterator>
#include <map>
#include <memory>
#include <set>
#include <string>

/** WWW-Authenticate to present with 401 Unauthorized response */
static const char *WWW_AUTH_HEADER_DATA = "Basic realm=\"jsonrpc\"";

/** RPC auth failure delay to make brute-forcing expensive */
static const int64_t RPC_AUTH_BRUTE_FORCE_DELAY = 250;

/**
 * Simple one-shot callback timer to be used by the RPC mechanism to e.g.
 * re-lock the wallet.
 */
class HTTPRPCTimer : public RPCTimerBase {
public:
    HTTPRPCTimer(struct event_base *eventBase, std::function<void()> &func,
                 int64_t millis)
        : ev(eventBase, false, func) {
        struct timeval tv;
        tv.tv_sec = millis / 1000;
        tv.tv_usec = (millis % 1000) * 1000;
        ev.trigger(&tv);
    }

private:
    HTTPEvent ev;
};

class HTTPRPCTimerInterface : public RPCTimerInterface {
public:
    explicit HTTPRPCTimerInterface(struct event_base *_base) : base(_base) {}

    const char *Name() override { return "HTTP"; }

    RPCTimerBase *NewTimer(std::function<void()> &func,
                           int64_t millis) override {
        return new HTTPRPCTimer(base, func, millis);
    }

private:
    struct event_base *base;
};

/* Pre-base64-encoded authentication token */
static std::string strRPCUserColonPass;
/* Pre-base64-encoded authentication token */
static std::string strRPCCORSDomain;
/* Stored RPC timer interface (for unregistration) */
static std::unique_ptr<HTTPRPCTimerInterface> httpRPCTimerInterface;
/* RPC Auth Whitelist */
static std::map<std::string, std::set<std::string>> g_rpc_whitelist;
static bool g_rpc_whitelist_default = false;

static void JSONErrorReply(HTTPRequest *req, const UniValue &objError,
                           const UniValue &id) {
    // Send error reply from json-rpc error object.
    int nStatus = HTTP_INTERNAL_SERVER_ERROR;
    int code = objError.find_value("code").getInt<int>();

    if (code == RPC_INVALID_REQUEST) {
        nStatus = HTTP_BAD_REQUEST;
    } else if (code == RPC_METHOD_NOT_FOUND) {
        nStatus = HTTP_NOT_FOUND;
    }

    std::string strReply = JSONRPCReply(NullUniValue, objError, id);

    req->WriteHeader("Content-Type", "application/json");
    req->WriteReply(nStatus, strReply);
}

/*
 * This function checks username and password against -rpcauth entries from
 * config file.
 */
static bool multiUserAuthorized(std::string strUserPass) {
    if (strUserPass.find(':') == std::string::npos) {
        return false;
    }
    std::string strUser = strUserPass.substr(0, strUserPass.find(':'));
    std::string strPass = strUserPass.substr(strUserPass.find(':') + 1);

    for (const std::string &strRPCAuth : gArgs.GetArgs("-rpcauth")) {
        // Search for multi-user login/pass "rpcauth" from config
        std::vector<std::string> vFields;
        boost::split(vFields, strRPCAuth, boost::is_any_of(":$"));
        if (vFields.size() != 3) {
            // Incorrect formatting in config file
            continue;
        }

        std::string strName = vFields[0];
        if (!TimingResistantEqual(strName, strUser)) {
            continue;
        }

        std::string strSalt = vFields[1];
        std::string strHash = vFields[2];

        static const unsigned int KEY_SIZE = 32;
        uint8_t out[KEY_SIZE];

        CHMAC_SHA256(reinterpret_cast<const uint8_t *>(strSalt.data()),
                     strSalt.size())
            .Write(reinterpret_cast<const uint8_t *>(strPass.data()),
                   strPass.size())
            .Finalize(out);
        std::vector<uint8_t> hexvec(out, out + KEY_SIZE);
        std::string strHashFromPass = HexStr(hexvec);

        if (TimingResistantEqual(strHashFromPass, strHash)) {
            return true;
        }
    }
    return false;
}

static bool RPCAuthorized(const std::string &strAuth,
                          std::string &strAuthUsernameOut) {
    // Belt-and-suspenders measure if InitRPCAuthentication was not called.
    if (strRPCUserColonPass.empty()) {
        return false;
    }

    if (strAuth.substr(0, 6) != "Basic ") {
        return false;
    }

    std::string_view strUserPass64 =
        TrimStringView(std::string_view{strAuth}.substr(6));
    auto userpass_data = DecodeBase64(strUserPass64);
    std::string strUserPass;
    if (!userpass_data) {
        return false;
    }
    strUserPass.assign(userpass_data->begin(), userpass_data->end());

    if (strUserPass.find(':') != std::string::npos) {
        strAuthUsernameOut = strUserPass.substr(0, strUserPass.find(':'));
    }

    // Check if authorized under single-user field
    if (TimingResistantEqual(strUserPass, strRPCUserColonPass)) {
        return true;
    }
    return multiUserAuthorized(strUserPass);
}

static bool checkCORS(HTTPRequest *req) {
    // https://www.w3.org/TR/cors/#resource-requests

    // 1. If the Origin header is not present terminate this set of steps.
    // The request is outside the scope of this specification.
    std::pair<bool, std::string> origin = req->GetHeader("origin");
    if (!origin.first) {
        return false;
    }

    // 2. If the value of the Origin header is not a case-sensitive match for
    // any of the values in list of origins do not set any additional headers
    // and terminate this set of steps.
    // Note: Always matching is acceptable since the list of origins can be
    // unbounded.
    if (origin.second != strRPCCORSDomain) {
        return false;
    }

    if (req->GetRequestMethod() == HTTPRequest::OPTIONS) {
        // 6.2 Preflight Request
        // In response to a preflight request the resource indicates which
        // methods and headers (other than simple methods and simple
        // headers) it is willing to handle and whether it supports
        // credentials.
        // Resources must use the following set of steps to determine which
        // additional headers to use in the response:

        // 3. Let method be the value as result of parsing the
        // Access-Control-Request-Method header.
        // If there is no Access-Control-Request-Method header or if parsing
        // failed, do not set any additional headers and terminate this set
        // of steps. The request is outside the scope of this specification.
        std::pair<bool, std::string> method =
            req->GetHeader("access-control-request-method");
        if (!method.first) {
            return false;
        }

        // 4. Let header field-names be the values as result of parsing
        // the Access-Control-Request-Headers headers.
        // If there are no Access-Control-Request-Headers headers let header
        // field-names be the empty list.
        // If parsing failed do not set any additional headers and terminate
        // this set of steps. The request is outside the scope of this
        // specification.
        std::pair<bool, std::string> header_field_names =
            req->GetHeader("access-control-request-headers");

        // 5. If method is not a case-sensitive match for any of the
        // values in list of methods do not set any additional headers
        // and terminate this set of steps.
        // Note: Always matching is acceptable since the list of methods
        // can be unbounded.
        if (method.second != "POST") {
            return false;
        }

        // 6. If any of the header field-names is not a ASCII case-
        // insensitive match for any of the values in list of headers do not
        // set any additional headers and terminate this set of steps.
        // Note: Always matching is acceptable since the list of headers can
        // be unbounded.
        const std::string &list_of_headers = "authorization,content-type";

        // 7. If the resource supports credentials add a single
        // Access-Control-Allow-Origin header, with the value of the Origin
        // header as value, and add a single
        // Access-Control-Allow-Credentials header with the case-sensitive
        // string "true" as value.
        req->WriteHeader("Access-Control-Allow-Origin", origin.second);
        req->WriteHeader("Access-Control-Allow-Credentials", "true");

        // 8. Optionally add a single Access-Control-Max-Age header with as
        // value the amount of seconds the user agent is allowed to cache
        // the result of the request.

        // 9. If method is a simple method this step may be skipped.
        // Add one or more Access-Control-Allow-Methods headers consisting
        // of (a subset of) the list of methods.
        // If a method is a simple method it does not need to be listed, but
        // this is not prohibited.
        // Note: Since the list of methods can be unbounded, simply
        // returning the method indicated by
        // Access-Control-Request-Method (if supported) can be enough.
        req->WriteHeader("Access-Control-Allow-Methods", method.second);

        // 10. If each of the header field-names is a simple header and none
        // is Content-Type, this step may be skipped.
        // Add one or more Access-Control-Allow-Headers headers consisting
        // of (a subset of) the list of headers.
        req->WriteHeader("Access-Control-Allow-Headers",
                         header_field_names.first ? header_field_names.second
                                                  : list_of_headers);
        req->WriteReply(HTTP_OK);
        return true;
    }

    // 6.1 Simple Cross-Origin Request, Actual Request, and Redirects
    // In response to a simple cross-origin request or actual request the
    // resource indicates whether or not to share the response.
    // If the resource has been relocated, it indicates whether to share its
    // new URL.
    // Resources must use the following set of steps to determine which
    // additional headers to use in the response:

    // 3. If the resource supports credentials add a single
    // Access-Control-Allow-Origin header, with the value of the Origin
    // header as value, and add a single Access-Control-Allow-Credentials
    // header with the case-sensitive string "true" as value.
    req->WriteHeader("Access-Control-Allow-Origin", origin.second);
    req->WriteHeader("Access-Control-Allow-Credentials", "true");

    // 4. If the list of exposed headers is not empty add one or more
    // Access-Control-Expose-Headers headers, with as values the header
    // field names given in the list of exposed headers.
    req->WriteHeader("Access-Control-Expose-Headers", "WWW-Authenticate");

    return false;
}

bool HTTPRPCRequestProcessor::ProcessHTTPRequest(HTTPRequest *req) {
    // First, check and/or set CORS headers
    if (checkCORS(req)) {
        return true;
    }

    // JSONRPC handles only POST
    if (req->GetRequestMethod() != HTTPRequest::POST) {
        req->WriteReply(HTTP_BAD_METHOD,
                        "JSONRPC server handles only POST requests");
        return false;
    }
    // Check authorization
    std::pair<bool, std::string> authHeader = req->GetHeader("authorization");
    if (!authHeader.first) {
        req->WriteHeader("WWW-Authenticate", WWW_AUTH_HEADER_DATA);
        req->WriteReply(HTTP_UNAUTHORIZED);
        return false;
    }

    JSONRPCRequest jreq;
    jreq.context = context;
    jreq.peerAddr = req->GetPeer().ToString();
    if (!RPCAuthorized(authHeader.second, jreq.authUser)) {
        LogPrintf("ThreadRPCServer incorrect password attempt from %s\n",
                  jreq.peerAddr);

        /**
         * Deter brute-forcing.
         * If this results in a DoS the user really shouldn't have their RPC
         * port exposed.
         */
        UninterruptibleSleep(
            std::chrono::milliseconds{RPC_AUTH_BRUTE_FORCE_DELAY});

        req->WriteHeader("WWW-Authenticate", WWW_AUTH_HEADER_DATA);
        req->WriteReply(HTTP_UNAUTHORIZED);
        return false;
    }

    try {
        // Parse request
        UniValue valRequest;
        if (!valRequest.read(req->ReadBody())) {
            throw JSONRPCError(RPC_PARSE_ERROR, "Parse error");
        }

        // Set the URI
        jreq.URI = req->GetURI();

        std::string strReply;
        bool user_has_whitelist = g_rpc_whitelist.count(jreq.authUser);
        if (!user_has_whitelist && g_rpc_whitelist_default) {
            LogPrintf("RPC User %s not allowed to call any methods\n",
                      jreq.authUser);
            req->WriteReply(HTTP_FORBIDDEN);
            return false;

            // singleton request
        } else if (valRequest.isObject()) {
            jreq.parse(valRequest);
            if (user_has_whitelist &&
                !g_rpc_whitelist[jreq.authUser].count(jreq.strMethod)) {
                LogPrintf("RPC User %s not allowed to call method %s\n",
                          jreq.authUser, jreq.strMethod);
                req->WriteReply(HTTP_FORBIDDEN);
                return false;
            }
            UniValue result = rpcServer.ExecuteCommand(config, jreq);

            // Send reply
            strReply = JSONRPCReply(result, NullUniValue, jreq.id);

            // array of requests
        } else if (valRequest.isArray()) {
            if (user_has_whitelist) {
                for (unsigned int reqIdx = 0; reqIdx < valRequest.size();
                     reqIdx++) {
                    if (!valRequest[reqIdx].isObject()) {
                        throw JSONRPCError(RPC_INVALID_REQUEST,
                                           "Invalid Request object");
                    } else {
                        const UniValue &request = valRequest[reqIdx].get_obj();
                        // Parse method
                        std::string strMethod =
                            request.find_value("method").get_str();
                        if (!g_rpc_whitelist[jreq.authUser].count(strMethod)) {
                            LogPrintf(
                                "RPC User %s not allowed to call method %s\n",
                                jreq.authUser, strMethod);
                            req->WriteReply(HTTP_FORBIDDEN);
                            return false;
                        }
                    }
                }
            }
            strReply = JSONRPCExecBatch(config, rpcServer, jreq,
                                        valRequest.get_array());
        } else {
            throw JSONRPCError(RPC_PARSE_ERROR, "Top-level object parse error");
        }

        req->WriteHeader("Content-Type", "application/json");
        req->WriteReply(HTTP_OK, strReply);
    } catch (const UniValue &objError) {
        JSONErrorReply(req, objError, jreq.id);
        return false;
    } catch (const std::exception &e) {
        JSONErrorReply(req, JSONRPCError(RPC_PARSE_ERROR, e.what()), jreq.id);
        return false;
    }
    return true;
}

static bool InitRPCAuthentication() {
    if (gArgs.GetArg("-rpcpassword", "") == "") {
        LogPrintf("Using random cookie authentication.\n");
        if (!GenerateAuthCookie(&strRPCUserColonPass)) {
            return false;
        }
    } else {
        LogPrintf("Config options rpcuser and rpcpassword will soon be "
                  "deprecated. Locally-run instances may remove rpcuser to use "
                  "cookie-based auth, or may be replaced with rpcauth. Please "
                  "see share/rpcauth for rpcauth auth generation.\n");
        strRPCUserColonPass = gArgs.GetArg("-rpcuser", "") + ":" +
                              gArgs.GetArg("-rpcpassword", "");
    }

    strRPCCORSDomain = gArgs.GetArg("-rpccorsdomain", "");

    if (gArgs.GetArg("-rpcauth", "") != "") {
        LogPrintf("Using rpcauth authentication.\n");
    }

    g_rpc_whitelist_default = gArgs.GetBoolArg("-rpcwhitelistdefault",
                                               gArgs.IsArgSet("-rpcwhitelist"));
    for (const std::string &strRPCWhitelist : gArgs.GetArgs("-rpcwhitelist")) {
        auto pos = strRPCWhitelist.find(':');
        std::string strUser = strRPCWhitelist.substr(0, pos);
        bool intersect = g_rpc_whitelist.count(strUser);
        std::set<std::string> &whitelist = g_rpc_whitelist[strUser];
        if (pos != std::string::npos) {
            std::string strWhitelist = strRPCWhitelist.substr(pos + 1);
            std::set<std::string> new_whitelist;
            boost::split(new_whitelist, strWhitelist, boost::is_any_of(", "));
            if (intersect) {
                std::set<std::string> tmp_whitelist;
                std::set_intersection(
                    new_whitelist.begin(), new_whitelist.end(),
                    whitelist.begin(), whitelist.end(),
                    std::inserter(tmp_whitelist, tmp_whitelist.end()));
                new_whitelist = std::move(tmp_whitelist);
            }
            whitelist = std::move(new_whitelist);
        }
    }

    return true;
}

bool StartHTTPRPC(HTTPRPCRequestProcessor &httpRPCRequestProcessor) {
    LogPrint(BCLog::RPC, "Starting HTTP RPC server\n");
    if (!InitRPCAuthentication()) {
        return false;
    }

    const std::function<bool(Config &, HTTPRequest *, const std::string &)>
        &rpcFunction =
            std::bind(&HTTPRPCRequestProcessor::DelegateHTTPRequest,
                      &httpRPCRequestProcessor, std::placeholders::_2);
    RegisterHTTPHandler("/", true, rpcFunction);
    if (g_wallet_init_interface.HasWalletSupport()) {
        RegisterHTTPHandler("/wallet/", false, rpcFunction);
    }
    struct event_base *eventBase = EventBase();
    assert(eventBase);
    httpRPCTimerInterface = std::make_unique<HTTPRPCTimerInterface>(eventBase);
    RPCSetTimerInterface(httpRPCTimerInterface.get());
    return true;
}

void InterruptHTTPRPC() {
    LogPrint(BCLog::RPC, "Interrupting HTTP RPC server\n");
}

void StopHTTPRPC() {
    LogPrint(BCLog::RPC, "Stopping HTTP RPC server\n");
    UnregisterHTTPHandler("/", true);
    if (g_wallet_init_interface.HasWalletSupport()) {
        UnregisterHTTPHandler("/wallet/", false);
    }
    if (httpRPCTimerInterface) {
        RPCUnsetTimerInterface(httpRPCTimerInterface.get());
        httpRPCTimerInterface.reset();
    }
}
